import BigNumber from 'bignumber.js'

import { Signer } from '@app/signer'
import { fromWei } from '@app/utils'
import { Accounts } from '@app/accounts'
import { OzTokenABI } from '@app/contract/oztoken'

import Tokens from '@config/tokens.json'
import * as config from '@config/server.json'

interface IToken {
    symbol: string
    contract: string
    decimals?: number
    hotwallet?: string
    hotaddress?: string
    notify?: string
}

class Wallet {
    public tronWeb: any
    public signer: Signer
    public accounts: Accounts
    readonly hotAddresses: Set<string>

    private trc20ABI: any
    private tokens: Map<string, IToken>
    private addr2Symbol: Map<string, string>
    private ownerAddress: string

    constructor(tronWeb: any, accounts: Accounts, signer: Signer) {
        this.tronWeb = tronWeb
        this.signer = signer
        this.accounts = accounts

        this.trc20ABI = JSON.parse(OzTokenABI)
        this.tokens = new Map<string, IToken>()
        this.hotAddresses = new Set<string>()
        this.addr2Symbol = new Map<string, string>()

        Tokens.forEach((value: any) => {
            const token: IToken = {
                symbol: value.symbol,
                contract: value.contract,
                hotwallet: value.hotwallet,
                notify: value.notify
            }
            this.tokens.set(value.symbol, token)
            this.addr2Symbol.set(value.contract, value.symbol)

            if (token.hotwallet) {
                const address = tronWeb.address.fromPrivateKey(token.hotwallet)
                token.hotaddress = address
                this.hotAddresses.add(address)
            }
        })

        this.ownerAddress = tronWeb.address.fromPrivateKey(config.hotwallet)
        this.hotAddresses.add(this.ownerAddress)
    }

    /**
     * 获取代币符号
     */
    getSymbols() {
        const tokens = new Array<string>('TRX')
        this.tokens.forEach((token => {
            tokens.push(token.symbol)
        }))
        return tokens
    }

    /**
     * 获取代币信息
     * @param symbol - 代币符号
     */
    async getToken(symbol: string) {
        const token = this.tokens.get(symbol)
        if (!token) {
            throw new Error('unknown token')
        }
        if (token.decimals) {
            return token
        }

        const instance = await this.tronWeb.contract(this.trc20ABI, token.contract)
        const decimals = await instance.decimals().call() as number

        token.decimals = decimals
        this.tokens.set(symbol, token)

        return token
     }

    /**
     * 根据合约地址获取代币
     * @param address - 合约地址
     */
    async getTokenByAddress(address: string) {
        const symbol = this.addr2Symbol.get(address)
        if (!symbol) {
            return
        }
        return this.getToken(symbol)
    }

    /**
     * 判断地址是否为本地钱包地址
     * @param address - 地址
     */
    isLocalAccount(address: string) {
        if (this.accounts.has(address)) {
            return true
        }
        return this.hotAddresses.has(address)
    }

    /**
     * 获取代币余额
     * @param symbol - 代币符号
     * @param address - 帐号地址
     */
    async getBalance(address: string, symbol: string) {
        if (symbol === 'TRX') {
            const balance = await this.tronWeb.trx.getBalance(address)
            return this.tronWeb.fromSun(balance) as string
        }

        const token = await this.getToken(symbol)
        if (!token || !token.decimals) {
            throw new Error('unknown token')
        }

        const instance = await this.tronWeb.contract(this.trc20ABI, token.contract)
        const balance = (await instance.balanceOf(address).call()).toString() as string
        return fromWei(new BigNumber(balance), token.decimals).toString()
    }

    /**
     * 获取地址私钥
     * @param address - 地址
     */
     async getPrivateKey(address: string) {
        if (address == this.ownerAddress) {
            return config.hotwallet
        }

        if (this.hotAddresses.has(address)) {
            let privateKey: string | undefined
            this.tokens.forEach((token) => {
                if (token.hotwallet && token.hotaddress === address) {
                    privateKey = token.hotwallet
                }
            })
            return privateKey
        }
        return this.accounts.getPrivateKey(address)
    }

    /**
     * 从热钱包转移TRX
     * @param to - 目标地址
     * @param amount - 发送金额
     */
    async transfer(to: string, amount: BigNumber) {
        return this.signer.transfer(config.hotwallet, to, amount)
    }

    /**
     * 从指定地址转移TRX
     * @param from - 来源地址
     * @param to - 目标地址
     * @param amount - 发送金额
     * @param privateKey - 地址私钥
     */
    async transferFrom(from: string, to: string, amount: BigNumber) {
        const privateKey = await this.getPrivateKey(from)
        if (!privateKey) {
            throw new Error('unlock account')
        }
        return this.signer.transfer(privateKey, to, amount)
    }

    /**
     * 从热钱包转移TRC20代币
     * @param symbol - 代币符号
     * @param to - 目标地址
     * @param amount - 发送金额
     */
    async transferToken(symbol: string, to: string, amount: BigNumber) {
        const token = await this.getToken(symbol)
        if (!token || !token.decimals) {
            throw new Error('invalid token symbol')
        }

        let privateKey = config.hotwallet
        if (token.hotwallet) {
            privateKey = token.hotwallet
        }

        const from = this.tronWeb.address.fromPrivateKey(privateKey)
        await this.mustSufficientBalance(from, symbol, amount)

        return this.signer.transferTRC20(token.contract, privateKey, to, amount, token.decimals)
    }

    /**
     * 从指定地址转移TRC20代币
     * @param symbol - 代币符号
     * @param from - 来源地址
     * @param to - 目标地址
     * @param amount - 发送金额
     * @param privateKey - 地址私钥
     */
    async transferTokenFrom(symbol: string, from: string, to: string, amount: BigNumber) {
        const token = await this.getToken(symbol)
        if (!token || !token.decimals) {
            throw new Error('invalid token symbol')
        }

        const privateKey = await this.getPrivateKey(from)
        if (!privateKey) {
            throw new Error('unlock account')
        }

        await this.mustSufficientBalance(from, symbol, amount)

        return this.signer.transferTRC20(token.contract, privateKey, to, amount, token.decimals)
    }

    /**
     * 检查地址余额是否充足，否则抛出异常
     * @param from - 来源地址
     * @param symbol - 代币符号
     * @param amount - 发送金额
     */
    private async mustSufficientBalance(from: string, symbol: string, amount: BigNumber) {
        const trx = new BigNumber(await this.getBalance(from, 'TRX'))
        if (trx.isLessThan(config.minTrxBalance)) {
            throw new Error('insufficient balance')
        }

        const balance = new BigNumber(await this.getBalance(from, symbol))
        if (balance.isLessThan(amount)) {
            throw new Error('insufficient balance')
        }
    }
}

export { Wallet }
